﻿using System;
using System.Activities;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using YasharEl.Infrastructure.Core.Applications;
using YasharEl.Infrastructure.Core.PersistenceModel.Applications;
using YasharEl.Infrastructure.Core.Prototype;
using YasharEl.Infrastructure.Diagnostics.Logging;
using YasharEl.Infrastructure.EntityModel;
using YasharEl.Infrastructure.ExceptionHandling;
using YasharEl.Infrastructure.ObjectFactory;
using YasharEl.Infrastructure.ProcessEngine.DomainModel.WorkflowManagement;
using YasharEl.Infrastructure.ProcessEngine.EntityModel;
using YasharEl.Infrastructure.ProcessEngine.PersistenceModel.Repositories;
using YasharEl.Infrastructure.ProcessEngine.PersistenceModel.Specifications;
using YasharEl.Infrastructure.ProcessEngine.Prototype;
using YasharEl.Infrastructure.ResourcesManagement;

namespace YasharEl.Infrastructure.ProcessEngine.DomainModel.Implementation
{
    public class DefaultProcessProcessor : IProcessProcessor
    {
        #region ReadOnly Fields

        private readonly IDependencyResolver resolver;
        private readonly ILogger<DefaultProcessProcessor> logger;
        private readonly IExceptionManager exceptionManager;
        private readonly IUnitOfWork unitOfWork;
        private readonly IDateTimeProvider dateTimeProvider;
        private readonly IProcessDefinitionsRepository processDefinitionsRepository;
        private readonly IApplicationsRepository applicationsRepository;
        private readonly IProcessParticipantsRepository participantsRepository;
        private readonly IProcessesRepository processesRepository;
        private readonly IWorkflowExecutionManager workflowExecutionManager;

        #endregion

        #region Constructors

        public DefaultProcessProcessor(
            IDependencyResolver resolver,
            ILogger<DefaultProcessProcessor> logger, 
            IExceptionManager exceptionManager,
            IUnitOfWork unitOfWork,
            IDateTimeProvider dateTimeProvider,
            IProcessDefinitionsRepository processDefinitionsRepository,
            IApplicationsRepository applicationsRepository,
            IProcessParticipantsRepository participantsRepository,
            IProcessesRepository processesRepository,
            IWorkflowExecutionManager workflowExecutionManager)
        {
            Contract.Requires<ArgumentNullException>(resolver != null, "resolver could not be null.");
            Contract.Requires<ArgumentNullException>(logger != null, "logger could not be null.");
            Contract.Requires<ArgumentNullException>(exceptionManager != null, "exceptionManager could not be null.");
            Contract.Requires<ArgumentNullException>(unitOfWork != null, "unitOfWork could not be null.");
            Contract.Requires<ArgumentNullException>(dateTimeProvider != null, "dateTimeProvider could not be null.");
            Contract.Requires<ArgumentNullException>(processDefinitionsRepository != null, "processDefinitionsRepository could not be null.");
            Contract.Requires<ArgumentNullException>(applicationsRepository != null, "applicationsRepository could not be null.");
            Contract.Requires<ArgumentNullException>(participantsRepository != null, "participantsRepository could not be null.");
            Contract.Requires<ArgumentNullException>(processesRepository != null, "processesRepository could not be null.");
            Contract.Requires<ArgumentNullException>(workflowExecutionManager != null, "workflowExecutionManager could not be null.");

            this.resolver = resolver;
            this.logger = logger;
            this.exceptionManager = exceptionManager;
            this.unitOfWork = unitOfWork;
            this.dateTimeProvider = dateTimeProvider;
            this.processDefinitionsRepository = processDefinitionsRepository;
            this.applicationsRepository = applicationsRepository;
            this.participantsRepository = participantsRepository;
            this.processesRepository = processesRepository;
            this.workflowExecutionManager = workflowExecutionManager;
        }

        #endregion

        #region IProcessProcessor Members

        public IProcess StartProcess(long applicationId, long userId, long processDefinitionId, long owner, string comment, Dictionary<string, object> inputs)
        {
            logger.Debug("Start process of definition {0} requested by user {1}...", processDefinitionId, userId);

            IProcess startedProcess = null;
            //Dictionary<string, object> workflowInputs = new Dictionary<string, object>();
            WorkflowApplication startedWorkflowInstance = null;

            try
            {
                IApplication application = applicationsRepository.GetById(applicationId);
                if (application == null)
                {
                    throw new ApplicationNotFoundException(applicationId);
                }

                IProcessParticipant user = participantsRepository.GetById(userId);
                if (user == null)
                {
                    throw new ProcessParticipantNotFoundException(
                        resolver.Resolve<IResourceManager>(), userId);
                }

                IProcessParticipant ownerUser = participantsRepository.GetById(owner);
                if (ownerUser == null)
                {
                    throw new ProcessParticipantNotFoundException(
                        resolver.Resolve<IResourceManager>(), owner);
                }

                IProcessDefinition processDefinition = processDefinitionsRepository.GetById(processDefinitionId);
                if (processDefinition != null)
                {
                    if (processDefinition.BelongsToGroup.BelongsToApplication != application)
                    {
                        logger.Warn("Process definition {0} requested for incorrect application reference {1} by user {2}.", processDefinitionId, applicationId, userId);
                        throw new ProcessDefinitionAccessDeniedException(
                            resolver.Resolve<IResourceManager>(),
                            applicationId,
                            processDefinitionId
                        );
                    }
                }

                #region Validate Required Parameters

                IList<IProcessDefinitionPropertyMapping> requiredProperties = 
                    (
                        from m in processDefinition.DefinedProperties
                        where m.IsRequired == true
                        select m as IProcessDefinitionPropertyMapping
                    )
                    .ToList();

                if( requiredProperties == null )
                    requiredProperties = new List<IProcessDefinitionPropertyMapping>();

                if (inputs == null)
                    inputs = new Dictionary<string, object>();

                foreach (IProcessDefinitionPropertyMapping propertyMapping in requiredProperties)
                {
                    if (!inputs.ContainsKey(propertyMapping.PropertyDefinition.Key))
                    {
                        throw new MissingRequiredParameterException(propertyMapping.PropertyDefinition.Key);
                    }

                    KeyValuePair<string, object> input = 
                        inputs.Where(k => k.Key == propertyMapping.PropertyDefinition.Key).First();
                }

                #endregion

                unitOfWork.ExecuteTransactional(
                    () =>
                    {
                        startedProcess = new ProcessEntity();
                        startedProcess.CreatedBy = user;
                        startedProcess.CreatedDate = dateTimeProvider.Now;
                        startedProcess.Owner = ownerUser;
                        startedProcess.ProcessDefinition = processDefinition;
                        startedProcess.ProcessPriority = ProcessPriority.Normal;
                        startedProcess.ProcessStatus = ProcessStatus.Active;
                        startedProcess.StartedDate = dateTimeProvider.Now;
                        startedProcess.ExpiredDate = dateTimeProvider.Now.AddDays(7);

                        WorkflowIdentity workflowIdentity = new WorkflowIdentity()
                        {
                            Name = processDefinition.Name,
                            Version = new Version(1,0,0,0)
                        };

                        startedWorkflowInstance = workflowExecutionManager.StartWorkflowSuspended(
                            processDefinition.WorkflowXaml,
                            inputs,
                            workflowIdentity
                        );

                        // Update workflow instance identifier to created workflow application instance
                        startedProcess.WorkflowInstanceId = startedWorkflowInstance.Id;

                        foreach (string parameterKey in inputs.Keys)
                        {
                            startedProcess.StoreValue(parameterKey, inputs[parameterKey], resolver);
                        }

                        processesRepository.Insert(startedProcess);
                    }
                );

                // Start previously created and suspended workflow application instance
                startedWorkflowInstance = workflowExecutionManager.RunWorkflow(startedWorkflowInstance);

                logger.Debug("Process {0} of definition {1} started for user {2} ...", startedProcess.ProcessId, processDefinitionId, userId);
            }
            catch (Exception e)
            {
                bool rethrow = exceptionManager.HandleException(e, ProcessEngineConstants.DomainLayerExceptionPolicy);
                if (rethrow)
                {
                    throw;
                }
            }

            return startedProcess;
        }

        public IProcess CancelProcess(long applicationId, long userId, Guid workflowInstanceId, string cancellationReason)
        {
            logger.Debug("Cancel process {0} requested by user {1}...", workflowInstanceId, userId);

            IProcess processToCancel = null;
            try
            {
                IApplication application = applicationsRepository.GetById(applicationId);
                if (application == null)
                {
                    throw new ApplicationNotFoundException(applicationId);
                }

                IProcessParticipant user = participantsRepository.GetById(userId);
                if (user == null)
                {
                    throw new ProcessParticipantNotFoundException(
                        resolver.Resolve<IResourceManager>(), userId);
                }

                processToCancel = processesRepository
                        .Specify<IProcessSpecification>()
                        .WithWorkflowInstanceId(workflowInstanceId)
                        .ToResult()
                        .SingleOrDefault();

                if (processToCancel == null)
                    throw new ProcessNotFoundException(
                        resolver.Resolve<IResourceManager>(),
                        workflowInstanceId
                    );

                if (processToCancel.ProcessDefinition.BelongsToGroup.BelongsToApplication != application)
                {
                    logger.Warn("Process {0} requested for incorrect application reference {1} by user {2}.", workflowInstanceId, applicationId, userId);
                    throw new ProcessDefinitionAccessDeniedException(
                        resolver.Resolve<IResourceManager>(),
                        applicationId,
                        processToCancel.ProcessDefinition.ProcessDefinitionId
                    );
                }

                // Only owner or creator of process allowed to cancel process instance...
                if (processToCancel.Owner != user && processToCancel.CreatedBy != user)
                {
                    throw new ProcessCancellationDeniedException(
                        resolver.Resolve<IResourceManager>(),
                        workflowInstanceId
                    );
                }

                if (processToCancel.ProcessStatus != ProcessStatus.Active)
                {
                    throw new ProcessWorkflowAlreadyCompletedException(
                        resolver.Resolve<IResourceManager>(),
                        workflowInstanceId
                    );
                }

                unitOfWork.ExecuteTransactional(() =>
                {
                    WorkflowIdentity workflowIdentity = new WorkflowIdentity()
                    {
                        Name = processToCancel.ProcessDefinition.Name,
                        Version = new Version(1,0,0,0)
                    };

                    WorkflowApplication workflowInstance = workflowExecutionManager.RetriveWorkflowInstance(
                        processToCancel.ProcessDefinition.WorkflowXaml,
                        workflowInstanceId,
                        workflowIdentity
                    );

                    workflowInstance.Cancel();

                    processToCancel.ProcessStatus = ProcessStatus.Cancelled;
                    processToCancel.ModifiedBy = user;
                    processToCancel.ModifiedDate = dateTimeProvider.Now;

                    // TODO : Store cancellation reason as comment here

                    processesRepository.Update(processToCancel);
                });
            }
            catch (Exception e)
            {
                bool rethrow = exceptionManager.HandleException(e, ProcessEngineConstants.DomainLayerExceptionPolicy);
                if (rethrow)
                {
                    throw;
                }
            }

            return processToCancel;
        }

        #endregion
    }
}
